Skip to main content

Lead Callback

The lead callback endpoint lets any system (a lead vendor, a lead-distribution platform, a CRM or your own backend) send residential solar leads into Merdeka. Each lead is created or updated in real time.

Each lead is a single POST with a flat JSON body. Merdeka matches it against existing records, so posting the same lead twice updates it rather than creating a duplicate.


Endpoint​

EnvironmentURL
Production (live)POST https://externals.merdeka.app/api/v1/lead/callback
Development (sandbox)POST https://externals-dev.merdeka.nvpdev.tech/api/v1/lead/callback

💡 Send to Development first. Once leads show up correctly in the sandbox, switch the URL to Production.


Authentication​

Every request must carry a Merdeka bearer token.

1. Get a token from the login endpoint (see Quick Start Push for details):

POST https://api.merdeka.app/api/v1/auth/email/login
{
"email": "your-integration-user@example.com",
"password": "your-password"
}

2. Send it on every callback:

Authorization: Bearer <your_token>
Content-Type: application/json
info

The token also lets Merdeka run lead-assignment rules on new leads. Leads that arrive without it can't be auto-assigned.

📩 Need an integration user? Email: merdeka@sunderenergy


Sending a Lead​

Whatever system you send from, set up the request like this:

  1. URL: the endpoint for your environment (see above).
  2. Method: POST.
  3. Headers: Authorization: Bearer <your_token> and Content-Type: application/json.
  4. Body: a flat JSON object, one lead per request. Map your fields to the Merdeka field names in the Field Reference below.
  5. Success check: treat the request as successful only when data.leadId is present in the response (see Responses).

Example Request​

curl -X POST "https://externals-dev.merdeka.nvpdev.tech/api/v1/lead/callback" \
-H "Authorization: Bearer <your_token>" \
-H "Content-Type: application/json" \
-d @lead.json
lead.json
{
"first_name": "John",
"last_name": "Doe",
"email": "john.doe@example.com",
"phone_1": "+1 (555) 010-0100",
"address_1": "123 Main St",
"city": "Springfield",
"state": "IL",
"postal_code": "62704",
"country": "USA",
"homeowner": "Yes",
"electric_company_name": "Ameren Illinois",
"electric_monthly_amount": "$185.00",
"lead_source": "Digital",
"lead_source_detail": "Google Search",
"lead_level": 1,
"lead_price": 45.0,
"is_exclusive": "Exclusive",
"vendor_id_cslr": "VENDOR-001",
"vendor_name": "Acme Leads",
"trustedform_cert_url": "https://cert.trustedform.com/abc123",
"tcpa_opt_in_cslr": "Yes",
"consent_status_cslr": true,
"utm_source": "google",
"utm_medium": "cpc",
"campaign_id": "spring-2026",
"landing_url": "https://example.com/solar",
"is_test": false
}

Field Reference​

All fields are optional at the API level, but Merdeka uses the Recommended fields to match and route leads. Send them whenever you have them.

tip

Values marked Configured list must exactly match a value set up in Merdeka. An unknown value is ignored rather than rejected. Ask the Merdeka team for the current list before you go live.

Contact​

FieldTypeRecommendedNotes
first_namestring✅
last_namestring✅
emailstring✅Used to match existing contacts at the same address.
phone_1string✅Primary phone. Any format; Merdeka compares digits only.
phone_2stringSecondary phone.
homeownerstringOwnership status of the contact (e.g. Yes / No).
how_did_you_hear_about_usstring

Property​

FieldTypeRecommendedNotes
address_1string✅Street address. Used to match existing properties.
address_2stringUnit / apartment.
citystring✅
statestring✅Two-letter US state code, e.g. CA.
postal_codestring✅Used to match existing properties.
countrystringDefaults to USA.
electric_company_namestringUtility company, free text.
electric_monthly_amountstring or numberAverage monthly bill. Currency symbols and commas are stripped ("$1,200" → 1200).
property_sun_exposurestring
property_typestringConfigured list.
roof_typestringConfigured list.

Lead​

FieldTypeRecommendedNotes
lead_sourcestring✅Configured list. Also part of how Merdeka recognises a repeat lead (see Duplicate handling).
lead_source_detailstringConfigured list.
lead_levelinteger✅Routing tier. Defaults to 1. Send a number (1), not a string ("1").
lead_pricenumberPrice paid for the lead.
is_exclusivestring"Shared" marks the lead as shared; any other value marks it exclusive.
product_typestring
company_brandstringConfigured list.
sdr_channelstringConfigured list.
credit_scorestringConfigured list (score band).
commentsstringFree-text notes.
promo_code_spwrstringPromo code.
is_testbooleanSend true for test leads. Any non-empty value counts as true, so omit the field (or send false) for real leads.

Vendor & compliance​

FieldTypeRecommendedNotes
vendor_id_cslrstringYour vendor ID.
vendor_namestring
trustedform_cert_urlstring✅TrustedForm certificate URL.
tcpa_opt_in_cslrstring✅TCPA opt-in value as captured.
consent_status_cslrbooleantrue when consent was given.

Attribution​

FieldTypeNotes
utm_sourcestring
utm_mediumstring
utm_campaignstring
utm_contentstring
utm_termstring
utm_idstringAlso stored as the ambassador ID.
campaign_idstringCampaign identifier.
omnichannel_campaignstring
landing_urlstring
conversion_urlstring
referrer_urlstring
google.clidstringGoogle click ID, sent as a nested object: "google": { "clid": "..." }.

Referral​

FieldTypeNotes
referred_by_first_name_cslrstring
referred_by_last_name_cslrstring
referred_by_emailstring
referred_by_phonestring
maastringMAA recipient.

Sales assignment (optional)​

FieldTypeNotes
setterstringSetter email. setter_email is also accepted.
closerstringCloser email. closer_email is also accepted.
setter_teamstring
closer_teamstring

Duplicate Handling​

Merdeka matches each callback in three steps, so re-sending a lead updates it instead of creating a copy:

  1. Property: an exact match on address_1 + address_2 + postal_code.
  2. Contact (at that property): first by email, then by phone_1 / phone_2 (digits only).
  3. Lead: the same contact with the same lead_source.

If nothing matches, a new record is created at that step.

warning

An update overwrites the lead with the values in the new callback. If you leave out a configured-list field (lead_source_detail, company_brand, sdr_channel, credit_score, property_type, roof_type), its existing value is cleared, and is_exclusive, is_test and consent_status_cslr fall back to their defaults. Always send the full lead, not just the fields that changed.

Contact attribution (utm_*, lead_source, lead_source_detail on the contact) is captured only the first time a contact is created and is not overwritten later.


Responses​

Responses use the standard Merdeka envelope.

Success: 201 Created

{
"message": "Success",
"status": 201,
"data": {
"leadId": "5d0c9a7e-2f1b-4c3e-9a8d-1b2c3d4e5f60",
"leadCreated": true,
"account": { "id": "…", "address1": "123 Main St", "zipCode": "62704" },
"contact": { "id": "…", "firstName": "John", "lastName": "Doe" }
}
}
  • leadCreated: true: a new lead was created.
  • leadCreated: false: an existing lead was updated (leadId is the existing lead).

Processing failure: still 201, but data is null:

{
"message": "Success",
"status": 201,
"data": null
}
warning

A failed lead still returns HTTP 201. Check that data.leadId is present before treating a lead as accepted; don't rely on the status code alone.

Authentication failure: 401 Unauthorized. The token is missing, invalid or expired. Log in again and update the Authorization header in your flow.


🛠️ Troubleshooting​

  • 401 Unauthorized: token missing or expired. Re-login and update the header.
  • data: null: the lead failed to process. Check the payload is valid JSON with flat field names, then send us the payload.
  • Lead created twice: the address, email/phone or lead_source differed between the two callbacks (see Duplicate Handling).
  • Source, brand or credit band missing in Merdeka: the value didn't exactly match the configured list.
  • Lead routed to the wrong queue: lead_level was sent as a string. Send it as a number.

📨 Still unclear? Send us your payload and response and we'll help you debug.